package org.groovymud.object.registry;

import grails.spring.BeanBuilder;
import groovy.lang.Binding;
import groovy.lang.GroovyObjectSupport;
import groovy.util.GroovyScriptEngine;
import groovy.util.ResourceException;
import groovy.util.ScriptException;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;

import org.apache.log4j.Logger;
import org.codehaus.groovy.control.CompilationFailedException;
import org.groovymud.object.Container;
import org.groovymud.object.MudObject;
import org.groovymud.object.ObjectLocation;
import org.groovymud.object.alive.Player;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;

import com.thoughtworks.xstream.XStream;

/* Copyright 2008 Matthew Corby-Eaglen
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License. 
 * You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0 
 *
 * Unless required by applicable law or agreed to in writing, software 
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 
 * See the License for the specific language governing permissions and 
 * limitations under the License. 
 */
/**
 * The MudObjectAttendant is responsible for loading, saving, registering and
 * configuring all mud objects in the game.
 * 
 * The object can have an id
 * 
 */
public class MudObjectAttendant implements ApplicationContextAware {

    private ObjectLocation theVoid;

    private ObjectLocation playerImpl;

    private String mudSpacePlayerLocation;

    private static final Logger logger = Logger.getLogger(MudObjectAttendant.class);

    private final Registry objectRegistry;

    private final XStream xStream;
    
    @Autowired(required = true)
    private GroovyScriptEngine groovyScriptEngine;
    
    private ApplicationContext applicationContext;

    @Autowired(required = true)
    public MudObjectAttendant(XStream xstream, Registry registry) {
        this.xStream = xstream;
        this.objectRegistry = registry;
    }

    /**
     * this method loads all the script files that contain beans into the
     * context when the attendant is loaded
     * 
     * filename for the script must end with "MudBeans.groovy"
     * 
     * e.g. /mudspace/my/weapons/WeaponMudBeans.groovy
     */
    public void initialize() {
        BeanBuilder builder = new BeanBuilder(getApplicationContext(), getGroovyScriptEngine().getGroovyClassLoader());
        try {
            logger.info("initializing..");
            builder.loadBeans("classpath*:**/*MudBeans.groovy");

        } catch (Exception e) {
            logger.error(e, e);
        } finally {
            setApplicationContext(builder.createApplicationContext());
        }
    }

    /**
     * loads a mudobject object and initialise it using a location object
     * 
     * if the object isn't in the context, the definition script is loaded and
     * then the context is checked again. if the context doesn't contain the
     * object then the registry is checked
     * 
     * scripts that require the attendant to use a new context should create the
     * context with a beanbuilder and set the attendants context.
     * 
     * make sure you set the parent context first on the builder
     * 
     * @param ObjectLocation
     *            location - representing where the object can be found in the
     *            spring container
     */
    public MudObject load(ObjectLocation location) {
        MudObject obj = null;
        if (!getApplicationContext().containsBean(location.getBeanId())) {
            logger.info("loading definition" + location.getDefinition());
            loadDefinition(location.getDefinition());
        }
        String beanId = location.getBeanId();

        obj = findOrLoad(beanId);

        return obj;
    }

    public MudObject findOrLoad(String beanId) {
        MudObject obj;
        if (!getApplicationContext().containsBean(beanId)) {
            // check the registry
            obj = getObjectRegistry().getMudObject(beanId);
            if (logger.isDebugEnabled()) {
                logger.debug("found beanId" + beanId + " in registry:" + obj);
            }
        } else {
            // otherwise check the application context
            obj = (MudObject) getApplicationContext().getBean(beanId);
            if (logger.isDebugEnabled()) {
                logger.debug("found beanId" + beanId + " in app context:" + obj);
            }
        }
        return obj;
    }

    /**
     * loads the groovy object definition file
     * 
     * objects could be instantiated straight into the registry, or can be added
     * to the context for future reference
     * 
     * provides the object definition script with three bindings:
     * 
     * builder - a bean builder
     * parentContext - the spring context, in case we want to pass it on 
     * registry - in case we want to add the objects directly to the mud
     * 
     * @param scriptLocation
     */
    protected void loadDefinition(String scriptLocation) {
        Binding binding = new Binding();

        binding.setVariable("context", getApplicationContext());
        binding.setVariable("registry", getObjectRegistry());
        binding.setVariable("attendant", this);
        binding.setVariable("gse", getGroovyScriptEngine());
        try {
            if (logger.isInfoEnabled()) {
                logger.info("loading script:" + scriptLocation);
            }
            Object obj = getGroovyScriptEngine().run(scriptLocation, binding);
            if (obj instanceof ApplicationContext) {
                setApplicationContext((ApplicationContext) obj);
            }
        } catch (ResourceException e) {
            logger.error("error loading scriptLocation: " + scriptLocation);
            logger.error(e, e);
        } catch (ScriptException e) {
            logger.error(e, e);
        }
    }

    public Player loadPlayerData(String username) throws CompilationFailedException, ResourceException, ScriptException, FileNotFoundException {
        logger.info("loading playerImpl object..");
        Player player = null;
        XStream xstream = getXStream();
        synchronized (this.getClass()) {
            load(getPlayerImpl());
            File playerFile = createFile(username);
            if (playerFile.exists()) {
                logger.info("loading player file");
                InputStream xmlIn = createFileInputStream(playerFile);
                player = (Player) xstream.fromXML(xmlIn);
                getObjectRegistry().register(player);
            }
        }
        return player;
    }

    protected InputStream createFileInputStream(File playerFile) throws FileNotFoundException {
        return new FileInputStream(playerFile);
    }

    protected File createFile(String username) {
        return new File(mudSpacePlayerLocation + username + ".xml");
    }

    public void movePlayerToLocation(Player player) throws InstantiationException, FileNotFoundException, CompilationFailedException {
        Container room;
        logger.info("moving player to location.");
        ObjectLocation location = player.getContainerLocation();

        room = (Container) getObjectRegistry().getMudObject(location.getBeanId());
        if (room == null) {
            logger.info("loading container");
            room = (Container) load(location);
        }
        if (room == null) {
            throw new InstantiationException("container was null!");
        }
        logger.info("adding player to container");
        room.addMudObject(player);
    }

    public void movePlayerToVoid(Player player) throws IOException, FileNotFoundException {
        try {
            logger.info("moving player to the void..");
            player.setContainerLocation(getTheVoid());
            movePlayerToLocation(player);
        } catch (CompilationFailedException e) {
            logger.error(e, e);
        } catch (InstantiationException e) {
            logger.error(e, e);
        }
    }

    public Player createNewPlayer(String username) throws InstantiationException, FileNotFoundException, CompilationFailedException {
        logger.info("creating new player");
        Player player = (Player) load(getPlayerImpl());
        String upperName = username.substring(0, 1).toUpperCase() + username.substring(1);

        player.setName(upperName);
        player.addShortName(upperName);
        player.addShortName(username);
        getObjectRegistry().register(player);

        return player;
    }

    public void savePlayerData(Player player) {
        logger.info("saving player..");
        XStream xstream = getXStream();
        try {
            xstream.omitField(GroovyObjectSupport.class, "metaClass");
            xstream.setMode(XStream.XPATH_RELATIVE_REFERENCES);
            synchronized (this.getClass()) {
                FileOutputStream out = new FileOutputStream(createFile(player.getPlayerCredentials().getUsername()));
                xstream.toXML(player, out);
            }

        } catch (Exception e) {
            try {
                player.getTerminalOutput().writeln(e.getMessage());
            } catch (IOException e1) {
                logger.error(e1, e1);
            }

            logger.error(e, e);
        }
    }

    public Registry getObjectRegistry() {
        return objectRegistry;
    }

    public String getMudSpacePlayerLocation() {
        return mudSpacePlayerLocation;
    }

    public void setMudSpacePlayerLocation(String mudSpacePlayerLocation) {
        this.mudSpacePlayerLocation = mudSpacePlayerLocation.trim();
    }

    public ObjectLocation getTheVoid() {
        return theVoid;
    }

    public void setTheVoid(ObjectLocation theVoid) {
        this.theVoid = theVoid;
    }

    public ObjectLocation getPlayerImpl() {
        return playerImpl;
    }

    public void setPlayerImpl(ObjectLocation playerImpl) {
        this.playerImpl = playerImpl;
    }

    public XStream getXStream() {
        return xStream;
    }

    public ApplicationContext getApplicationContext() {
        return applicationContext;
    }

    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }

    public GroovyScriptEngine getGroovyScriptEngine() {
        return groovyScriptEngine;
    }

    public void setGroovyScriptEngine(GroovyScriptEngine groovyScriptEngine) {
        this.groovyScriptEngine = groovyScriptEngine;
    }

}
